有在寫 node 的人可能聽人提過, node 的底層是一個支援非同步 IO 的 threadpool , 而有讀過我前三篇的讀者應該可以聯想到 epoll 和 IOCP 兩種非同步 IO model 的現象, 就是創建一群閒置的 thread , 每當有一個 IO 發生就喚醒一個新的 thread 來運行。如果 threadpool 每個 thread 都在使用中, 就先讓 IO 事件排好隊等著。我們今天就試著讓 node 呈現這種現象。
實驗步驟
用 express 寫了一下 API
import express = require('express');
const router = express.Router();
router.get('/', (req: express.Request, res: express.Response) => {
// Disable caching for content files
res.header('Cache-Control', 'no-cache, no-store, must-revalidate');
res.header('Pragma', 'no-cache');
res.header('Expires', '0');
res.render('index', { title: 'Express' });
});
export default router;
再寫一個沒有功能但有大量內容的前端頁面
extends layout
block content
h1= title
p Welcome to #{title}
h1= title
p Welcome to #{title}
h1= title
p Welcome to #{title}
h1= title
p Welcome to #{title}
h1= title
p Welcome to #{title}
h1= title
p Welcome to #{title}
h1= title
p Welcome to #{title}
h1= title
p Welcome to #{title}
h1= title
// 往下延伸 5000 行
再寫一個 node 應用, 呼叫 API
const fetch = require('node-fetch');
for (let i = 0; i < 50; i++)
fetch('http://127.0.0.1:1337/', { method: 'GET' }).then((res) => {
if (res.status == 200) {
const time = new Date();
console.log('success : ' + time);
} else {
const time = new Date();
console.log('error : ' + time);
}
});
次數設定成同時呼叫 50 次
可以看到這兩個應用程式的 process 正在運行。
觀察 API server 的 thread 狀態
API 被呼叫前
API 正在運作時
可以理解為 thread 31612 是 main thread
其他 4 條是幫助他完成任務的 threadpool
而 threadpool 在這裡做的就是讀取網頁頁面與進行渲染, main thread 做的是進行 network IO
可以發現由 main thread 發出 IO 後, 就只有 main Thread 在等待回傳, threadpool 基本都在閒置
router.get(
'/',
(req: express.Request, res: express.Response, next: express.NextFunction) => {
console.log('In');
next();
},
(req: express.Request, res: express.Response) => {
setTimeout(() => {
// Disable caching for content files
console.log(new Date());
res.header('Cache-Control', 'no-cache, no-store, must-revalidate');
res.header('Pragma', 'no-cache');
res.header('Expires', '0');
res.render('index', { title: 'Express' });
}, 5000);
}
);
結果是
所以總共只有停下來等待了 5 秒鐘, 因為 Network IO 會被 main thread 掌管, 其沒有數量限制的瘋狂切換, 以近乎同時的方法處理所有 IO , 而本地資料抓取與渲染, 會被 TP 一個一個處理, 處理完再交給 main thread 回傳。而 TP 的數量是 4 個 thread
這個現象就像我昨天寫的 IOCP httpServer , main thread 瘋狂的接收 Network IO , 和把 NetWork IO 的事件放入 eventQueue, thread pool 一個一個醒來, 各取一個事件處理與回傳。
當然兩件事還是有蠻大的差別, 千萬別覺得他們差不多。(ex: node 的回傳會丟回來給 main thread 傳, node 的事件拆分沒有那麼粗糙, 所以可以做到更細緻的事件切換 ,.......)
實驗就到這裡, 基本上可以利用 epoll 或 IOCP 這類演算法來解釋, 但實際上 node 底層更為複雜, 就讓我們繼續看下面的附註吧
懶人包 :
因為這個現象, 有些人在撰寫 micro service 中具有大量 IO 的微服務時, 會試著提高 node 的 thread 數量。但這篇文章說, 網路 IO 都在 main thread 完成, 所以提高 node 的 thread 數量沒啥用。
Node 的非同步不是單純的非同步 IO , 而是所有可以非同步的事件被分成兩大類, 一類是只能在 main thread 內進行, 但 main thread 卻會在自己一條 thread 內快速切換任務完成所有事情, 舉例就是 network IO , 另一種就是會喚醒沉睡的 thread ( in threadpool ) 且要求他們執行 (但其實這類事件不全是 IO , 但為了方便之後還是統一稱為 IO ) , 像是本地檔案讀寫。
簡單看看一個用 JS 撰寫的 model 如何調用底層的 C++
明天見 !